/*!
 * # Fomantic-UI - Visibility
 * https://github.com/fomantic/Fomantic-UI/
 *
 *
 * Released under the MIT license
 * https://opensource.org/licenses/MIT
 *
 */

(function ($, window, document) {
    'use strict';

    function isFunction(obj) {
        return typeof obj === 'function' && typeof obj.nodeType !== 'number';
    }

    window = window !== undefined && window.Math === Math
        ? window
        : globalThis;

    $.fn.visibility = function (parameters) {
        var
            $allModules    = $(this),

            time           = Date.now(),
            performance    = [],

            query          = arguments[0],
            methodInvoked  = typeof query === 'string',
            queryArguments = [].slice.call(arguments, 1),
            contextCheck   = function (context, win) {
                var $context;
                if ([window, document].indexOf(context) >= 0) {
                    $context = $(context);
                } else {
                    $context = $(win.document).find(context);
                    if ($context.length === 0) {
                        $context = win.frameElement ? contextCheck(context, win.parent) : window;
                    }
                }

                return $context;
            },
            returnedValue,

            moduleCount    = $allModules.length,
            loadedCount    = 0
        ;

        $allModules.each(function () {
            var
                settings        = $.isPlainObject(parameters)
                    ? $.extend(true, {}, $.fn.visibility.settings, parameters)
                    : $.extend({}, $.fn.visibility.settings),

                className       = settings.className,
                namespace       = settings.namespace,
                error           = settings.error,
                metadata        = settings.metadata,

                eventNamespace  = '.' + namespace,
                moduleNamespace = 'module-' + namespace,

                $window         = $(window),

                $module         = $(this),
                $context        = contextCheck(settings.context, window),

                $placeholder,

                instance        = $module.data(moduleNamespace),

                element         = this,
                disabled        = false,

                contextObserver,
                observer,
                module
            ;

            module = {

                initialize: function () {
                    module.debug('Initializing', settings);

                    module.setup.cache();

                    if (module.should.trackChanges()) {
                        if (settings.type === 'image') {
                            module.setup.image();
                        }
                        if (settings.type === 'fixed') {
                            module.setup.fixed();
                        }

                        if (settings.observeChanges) {
                            module.observeChanges();
                        }
                        module.bind.events();
                    }

                    module.save.position();
                    if (!module.is.visible()) {
                        module.error(error.visible, $module);
                    }

                    if (settings.initialCheck) {
                        module.checkVisibility();
                    }
                    module.instantiate();
                },

                instantiate: function () {
                    module.debug('Storing instance', module);
                    $module
                        .data(moduleNamespace, module)
                    ;
                    instance = module;
                },

                destroy: function () {
                    module.verbose('Destroying previous module');
                    if (observer) {
                        observer.disconnect();
                    }
                    if (contextObserver) {
                        contextObserver.disconnect();
                    }
                    $window
                        .off('load' + eventNamespace, module.event.load)
                        .off('resize' + eventNamespace, module.event.resize)
                    ;
                    $context
                        .off('scroll' + eventNamespace, module.event.scroll)
                        .off('scrollchange' + eventNamespace, module.event.scrollchange)
                    ;
                    if (settings.type === 'fixed') {
                        module.resetFixed();
                        module.remove.placeholder();
                    }
                    $module
                        .off(eventNamespace)
                        .removeData(moduleNamespace)
                    ;
                },

                observeChanges: function () {
                    if ('MutationObserver' in window) {
                        contextObserver = new MutationObserver(module.event.contextChanged);
                        observer = new MutationObserver(module.event.changed);
                        contextObserver.observe(document, {
                            childList: true,
                            subtree: true,
                        });
                        observer.observe(element, {
                            childList: true,
                            subtree: true,
                        });
                        module.debug('Setting up mutation observer', observer);
                    }
                },

                bind: {
                    events: function () {
                        module.verbose('Binding visibility events to scroll and resize');
                        if (settings.refreshOnLoad) {
                            $window
                                .on('load' + eventNamespace, module.event.load)
                            ;
                        }
                        $window
                            .on('resize' + eventNamespace, module.event.resize)
                        ;
                        // pub/sub pattern
                        $context
                            .off('scroll' + eventNamespace)
                            .on('scroll' + eventNamespace, module.event.scroll)
                            .on('scrollchange' + eventNamespace, module.event.scrollchange)
                        ;
                    },
                },

                event: {
                    changed: function (mutations) {
                        module.verbose('DOM tree modified, updating visibility calculations');
                        module.timer = setTimeout(function () {
                            module.verbose('DOM tree modified, updating sticky menu');
                            module.refresh();
                        }, 100);
                    },
                    contextChanged: function (mutations) {
                        [].forEach.call(mutations, function (mutation) {
                            if (mutation.removedNodes) {
                                [].forEach.call(mutation.removedNodes, function (node) {
                                    if (node === element || $(node).find(element).length > 0) {
                                        module.debug('Element removed from DOM, tearing down events');
                                        module.destroy();
                                    }
                                });
                            }
                        });
                    },
                    resize: function () {
                        module.debug('Window resized');
                        if (settings.refreshOnResize) {
                            requestAnimationFrame(module.refresh);
                        }
                    },
                    load: function () {
                        module.debug('Page finished loading');
                        requestAnimationFrame(module.refresh);
                    },
                    // publishes scrollchange event on one scroll
                    scroll: function () {
                        if (settings.throttle) {
                            clearTimeout(module.timer);
                            module.timer = setTimeout(function () {
                                $context.triggerHandler('scrollchange' + eventNamespace, [$context.scrollTop()]);
                            }, settings.throttle);
                        } else {
                            requestAnimationFrame(function () {
                                $context.triggerHandler('scrollchange' + eventNamespace, [$context.scrollTop()]);
                            });
                        }
                    },
                    // subscribes to scrollchange
                    scrollchange: function (event, scrollPosition) {
                        module.checkVisibility(scrollPosition);
                    },
                },

                precache: function (images, callback) {
                    if (!Array.isArray(images)) {
                        images = [images];
                    }
                    var
                        imagesLength  = images.length,
                        loadedCounter = 0,
                        cache         = [],
                        cacheImage    = document.createElement('img'),
                        handleLoad    = function () {
                            loadedCounter++;
                            if (loadedCounter >= images.length) {
                                if (isFunction(callback)) {
                                    callback();
                                }
                            }
                        }
                    ;
                    while (imagesLength--) {
                        cacheImage = document.createElement('img');
                        cacheImage.addEventListener('load', handleLoad);
                        cacheImage.addEventListener('error', handleLoad);
                        cacheImage.src = images[imagesLength];
                        cache.push(cacheImage);
                    }
                },

                enableCallbacks: function () {
                    module.debug('Allowing callbacks to occur');
                    disabled = false;
                },

                disableCallbacks: function () {
                    module.debug('Disabling all callbacks temporarily');
                    disabled = true;
                },

                should: {
                    trackChanges: function () {
                        if (methodInvoked) {
                            module.debug('One time query, no need to bind events');

                            return false;
                        }
                        module.debug('Callbacks being attached');

                        return true;
                    },
                },

                setup: {
                    cache: function () {
                        module.cache = {
                            occurred: {},
                            screen: {},
                            element: {},
                        };
                    },
                    image: function () {
                        var
                            src = $module.data(metadata.src)
                        ;
                        if (src) {
                            module.verbose('Lazy loading image', src);
                            settings.once = true;
                            settings.observeChanges = false;

                            // show when top visible
                            settings.onOnScreen = function () {
                                module.debug('Image on screen', element);
                                module.precache(src, function () {
                                    module.set.image(src, function () {
                                        loadedCount++;
                                        if (loadedCount === moduleCount) {
                                            settings.onAllLoaded.call(this);
                                        }
                                        settings.onLoad.call(this);
                                    });
                                });
                            };
                        }
                    },
                    fixed: function () {
                        module.debug('Setting up fixed');
                        settings.once = false;
                        settings.observeChanges = false;
                        settings.initialCheck = true;
                        settings.refreshOnLoad = true;
                        if (!parameters.transition) {
                            settings.transition = false;
                        }
                        module.create.placeholder();
                        module.debug('Added placeholder', $placeholder);
                        settings.onTopPassed = function () {
                            module.debug('Element passed, adding fixed position', $module);
                            module.show.placeholder();
                            module.set.fixed();
                            if (settings.transition) {
                                if ($.fn.transition !== undefined) {
                                    $module.transition(settings.transition, settings.duration);
                                }
                            }
                        };
                        settings.onTopPassedReverse = function () {
                            module.debug('Element returned to position, removing fixed', $module);
                            module.hide.placeholder();
                            module.remove.fixed();
                        };
                    },
                },

                create: {
                    placeholder: function () {
                        module.verbose('Creating fixed position placeholder');
                        $placeholder = $module
                            .clone(false)
                            .css('display', 'none')
                            .addClass(className.placeholder)
                            .insertAfter($module)
                        ;
                    },
                },

                show: {
                    placeholder: function () {
                        module.verbose('Showing placeholder');
                        $placeholder
                            .css('display', 'block')
                            .css('visibility', 'hidden')
                        ;
                    },
                },
                hide: {
                    placeholder: function () {
                        module.verbose('Hiding placeholder');
                        $placeholder
                            .css('display', 'none')
                            .css('visibility', '')
                        ;
                    },
                },

                set: {
                    fixed: function () {
                        module.verbose('Setting element to fixed position');
                        $module
                            .addClass(className.fixed)
                            .css({
                                position: 'fixed',
                                top: settings.offset + 'px',
                                left: 'auto',
                                zIndex: settings.zIndex,
                            })
                        ;
                        settings.onFixed.call(element);
                    },
                    image: function (src, callback) {
                        $module
                            .attr('src', src)
                        ;
                        if (settings.transition) {
                            if ($.fn.transition !== undefined) {
                                if ($module.hasClass(className.visible)) {
                                    module.debug('Transition already occurred on this image, skipping animation');

                                    return;
                                }
                                $module.transition(settings.transition, settings.duration, callback);
                            } else {
                                $module.fadeIn(settings.duration, callback);
                            }
                        } else {
                            $module.show();
                        }
                    },
                },

                is: {
                    onScreen: function () {
                        var
                            calculations   = module.get.elementCalculations()
                        ;

                        return calculations.onScreen;
                    },
                    offScreen: function () {
                        var
                            calculations   = module.get.elementCalculations()
                        ;

                        return calculations.offScreen;
                    },
                    visible: function () {
                        if (module.cache && module.cache.element) {
                            return !(module.cache.element.width === 0 && module.cache.element.offset.top === 0);
                        }

                        return false;
                    },
                    verticallyScrollableContext: function () {
                        var
                            overflowY = $context[0] !== window
                                ? $context.css('overflow-y')
                                : false
                        ;

                        return overflowY === 'auto' || overflowY === 'scroll';
                    },
                    horizontallyScrollableContext: function () {
                        var
                            overflowX = $context[0] !== window
                                ? $context.css('overflow-x')
                                : false
                        ;

                        return overflowX === 'auto' || overflowX === 'scroll';
                    },
                },

                refresh: function () {
                    module.debug('Refreshing constants (width/height)');
                    if (settings.type === 'fixed') {
                        module.resetFixed();
                    }
                    module.reset();
                    module.save.position();
                    if (settings.checkOnRefresh) {
                        module.checkVisibility();
                    }
                    settings.onRefresh.call(element);
                },

                resetFixed: function () {
                    module.remove.fixed();
                    module.remove.occurred();
                },

                reset: function () {
                    module.verbose('Resetting all cached values');
                    if ($.isPlainObject(module.cache)) {
                        module.cache.screen = {};
                        module.cache.element = {};
                    }
                },

                checkVisibility: function (scroll) {
                    module.verbose('Checking visibility of element', module.cache.element);

                    if (!disabled && module.is.visible()) {
                        // save scroll position
                        module.save.scroll(scroll);

                        // update calculations derived from scroll
                        module.save.calculations();

                        // percentage
                        module.passed();

                        // reverse (must be first)
                        module.passingReverse();
                        module.topVisibleReverse();
                        module.bottomVisibleReverse();
                        module.topPassedReverse();
                        module.bottomPassedReverse();

                        // one time
                        module.onScreen();
                        module.offScreen();
                        module.passing();
                        module.topVisible();
                        module.bottomVisible();
                        module.topPassed();
                        module.bottomPassed();

                        // on update callback
                        if (settings.onUpdate) {
                            settings.onUpdate.call(element, module.get.elementCalculations());
                        }
                    }
                },

                passed: function (amount, newCallback) {
                    var
                        calculations   = module.get.elementCalculations()
                    ;
                    // assign callback
                    if (amount && newCallback) {
                        settings.onPassed[amount] = newCallback;
                    } else if (amount !== undefined) {
                        return module.get.pixelsPassed(amount) > calculations.pixelsPassed;
                    } else if (calculations.passing) {
                        $.each(settings.onPassed, function (amount, callback) {
                            if (calculations.bottomVisible || calculations.pixelsPassed > module.get.pixelsPassed(amount)) {
                                module.execute(callback, amount);
                            } else if (!settings.once) {
                                module.remove.occurred(callback);
                            }
                        });
                    }
                },

                onScreen: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onOnScreen,
                        callbackName = 'onScreen'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for onScreen', newCallback);
                        settings.onOnScreen = newCallback;
                    }
                    if (calculations.onScreen) {
                        module.execute(callback, callbackName);
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback !== undefined) {
                        return calculations.onOnScreen;
                    }
                },

                offScreen: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onOffScreen,
                        callbackName = 'offScreen'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for offScreen', newCallback);
                        settings.onOffScreen = newCallback;
                    }
                    if (calculations.offScreen) {
                        module.execute(callback, callbackName);
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback !== undefined) {
                        return calculations.onOffScreen;
                    }
                },

                passing: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onPassing,
                        callbackName = 'passing'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for passing', newCallback);
                        settings.onPassing = newCallback;
                    }
                    if (calculations.passing) {
                        module.execute(callback, callbackName);
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback !== undefined) {
                        return calculations.passing;
                    }
                },

                topVisible: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onTopVisible,
                        callbackName = 'topVisible'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for top visible', newCallback);
                        settings.onTopVisible = newCallback;
                    }
                    if (calculations.topVisible) {
                        module.execute(callback, callbackName);
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback === undefined) {
                        return calculations.topVisible;
                    }
                },

                bottomVisible: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onBottomVisible,
                        callbackName = 'bottomVisible'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for bottom visible', newCallback);
                        settings.onBottomVisible = newCallback;
                    }
                    if (calculations.bottomVisible) {
                        module.execute(callback, callbackName);
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback === undefined) {
                        return calculations.bottomVisible;
                    }
                },

                topPassed: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onTopPassed,
                        callbackName = 'topPassed'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for top passed', newCallback);
                        settings.onTopPassed = newCallback;
                    }
                    if (calculations.topPassed) {
                        module.execute(callback, callbackName);
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback === undefined) {
                        return calculations.topPassed;
                    }
                },

                bottomPassed: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onBottomPassed,
                        callbackName = 'bottomPassed'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for bottom passed', newCallback);
                        settings.onBottomPassed = newCallback;
                    }
                    if (calculations.bottomPassed) {
                        module.execute(callback, callbackName);
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback === undefined) {
                        return calculations.bottomPassed;
                    }
                },

                passingReverse: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onPassingReverse,
                        callbackName = 'passingReverse'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for passing reverse', newCallback);
                        settings.onPassingReverse = newCallback;
                    }
                    if (!calculations.passing) {
                        if (module.get.occurred('passing')) {
                            module.execute(callback, callbackName);
                        }
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback !== undefined) {
                        return !calculations.passing;
                    }
                },

                topVisibleReverse: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onTopVisibleReverse,
                        callbackName = 'topVisibleReverse'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for top visible reverse', newCallback);
                        settings.onTopVisibleReverse = newCallback;
                    }
                    if (!calculations.topVisible) {
                        if (module.get.occurred('topVisible')) {
                            module.execute(callback, callbackName);
                        }
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback === undefined) {
                        return !calculations.topVisible;
                    }
                },

                bottomVisibleReverse: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onBottomVisibleReverse,
                        callbackName = 'bottomVisibleReverse'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for bottom visible reverse', newCallback);
                        settings.onBottomVisibleReverse = newCallback;
                    }
                    if (!calculations.bottomVisible) {
                        if (module.get.occurred('bottomVisible')) {
                            module.execute(callback, callbackName);
                        }
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback === undefined) {
                        return !calculations.bottomVisible;
                    }
                },

                topPassedReverse: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onTopPassedReverse,
                        callbackName = 'topPassedReverse'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for top passed reverse', newCallback);
                        settings.onTopPassedReverse = newCallback;
                    }
                    if (!calculations.topPassed) {
                        if (module.get.occurred('topPassed')) {
                            module.execute(callback, callbackName);
                        }
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback === undefined) {
                        return !calculations.onTopPassed;
                    }
                },

                bottomPassedReverse: function (newCallback) {
                    var
                        calculations = module.get.elementCalculations(),
                        callback     = newCallback || settings.onBottomPassedReverse,
                        callbackName = 'bottomPassedReverse'
                    ;
                    if (newCallback) {
                        module.debug('Adding callback for bottom passed reverse', newCallback);
                        settings.onBottomPassedReverse = newCallback;
                    }
                    if (!calculations.bottomPassed) {
                        if (module.get.occurred('bottomPassed')) {
                            module.execute(callback, callbackName);
                        }
                    } else if (!settings.once) {
                        module.remove.occurred(callbackName);
                    }
                    if (newCallback === undefined) {
                        return !calculations.bottomPassed;
                    }
                },

                execute: function (callback, callbackName) {
                    var
                        calculations = module.get.elementCalculations(),
                        screen       = module.get.screenCalculations()
                    ;
                    callback = callback || false;
                    if (callback) {
                        if (settings.continuous) {
                            module.debug('Callback being called continuously', callbackName, calculations);
                            callback.call(element, calculations, screen);
                        } else if (!module.get.occurred(callbackName)) {
                            module.debug('Conditions met', callbackName, calculations);
                            callback.call(element, calculations, screen);
                        }
                    }
                    module.save.occurred(callbackName);
                },

                remove: {
                    fixed: function () {
                        module.debug('Removing fixed position');
                        $module
                            .removeClass(className.fixed)
                            .css({
                                position: '',
                                top: '',
                                left: '',
                                zIndex: '',
                            })
                        ;
                        settings.onUnfixed.call(element);
                    },
                    placeholder: function () {
                        module.debug('Removing placeholder content');
                        if ($placeholder) {
                            $placeholder.remove();
                        }
                    },
                    occurred: function (callback) {
                        if (callback) {
                            var
                                occurred = module.cache.occurred
                            ;
                            if (occurred[callback] !== undefined && occurred[callback] === true) {
                                module.debug('Callback can now be called again', callback);
                                module.cache.occurred[callback] = false;
                            }
                        } else {
                            module.cache.occurred = {};
                        }
                    },
                },

                save: {
                    calculations: function () {
                        module.verbose('Saving all calculations necessary to determine positioning');
                        module.save.direction();
                        module.save.screenCalculations();
                        module.save.elementCalculations();
                    },
                    occurred: function (callback) {
                        if (callback) {
                            if (module.cache.occurred[callback] === undefined || (module.cache.occurred[callback] !== true)) {
                                module.verbose('Saving callback occurred', callback);
                                module.cache.occurred[callback] = true;
                            }
                        }
                    },
                    scroll: function (scrollPosition) {
                        scrollPosition = scrollPosition + settings.offset || $context.scrollTop() + settings.offset;
                        module.cache.scroll = scrollPosition;
                    },
                    direction: function () {
                        var
                            scroll     = module.get.scroll(),
                            lastScroll = module.get.lastScroll(),
                            direction
                        ;
                        if (scroll > lastScroll && lastScroll) {
                            direction = 'down';
                        } else if (scroll < lastScroll && lastScroll) {
                            direction = 'up';
                        } else {
                            direction = 'static';
                        }
                        module.cache.direction = direction;

                        return module.cache.direction;
                    },
                    elementPosition: function () {
                        var
                            element = module.cache.element,
                            screen  = module.get.screenSize()
                        ;
                        module.verbose('Saving element position');
                        // (quicker than $.extend)
                        element.fits = element.height < screen.height;
                        element.offset = $module.offset();
                        element.width = $module.outerWidth();
                        element.height = $module.outerHeight();
                        // compensate for scroll in context
                        if (module.is.verticallyScrollableContext()) {
                            element.offset.top += $context.scrollTop() - $context.offset().top;
                        }
                        if (module.is.horizontallyScrollableContext()) {
                            element.offset.left += $context.scrollLeft() - $context.offset().left;
                        }
                        // store
                        module.cache.element = element;

                        return element;
                    },
                    elementCalculations: function () {
                        var
                            screen     = module.get.screenCalculations(),
                            element    = module.get.elementPosition()
                        ;
                        // offset
                        if (settings.includeMargin) {
                            element.margin = {};
                            element.margin.top = parseInt($module.css('margin-top'), 10);
                            element.margin.bottom = parseInt($module.css('margin-bottom'), 10);
                            element.top = element.offset.top - element.margin.top;
                            element.bottom = element.offset.top + element.height + element.margin.bottom;
                        } else {
                            element.top = element.offset.top;
                            element.bottom = element.offset.top + element.height;
                        }

                        // visibility
                        element.topPassed = screen.top >= element.top;
                        element.bottomPassed = screen.top >= element.bottom;
                        element.topVisible = (screen.bottom >= element.top) && !element.topPassed;
                        element.bottomVisible = (screen.bottom >= element.bottom) && !element.bottomPassed;
                        element.pixelsPassed = 0;
                        element.percentagePassed = 0;

                        // meta calculations
                        element.onScreen = (element.topVisible || element.passing) && !element.bottomPassed;
                        element.passing = element.topPassed && !element.bottomPassed;
                        element.offScreen = !element.onScreen;

                        // passing calculations
                        if (element.passing) {
                            element.pixelsPassed = screen.top - element.top;
                            element.percentagePassed = (screen.top - element.top) / element.height;
                        }
                        module.cache.element = element;
                        module.verbose('Updated element calculations', element);

                        return element;
                    },
                    screenCalculations: function () {
                        var
                            scroll = module.get.scroll()
                        ;
                        module.save.direction();
                        module.cache.screen.top = scroll;
                        module.cache.screen.bottom = scroll + module.cache.screen.height;

                        return module.cache.screen;
                    },
                    screenSize: function () {
                        module.verbose('Saving window position');
                        module.cache.screen = {
                            height: $context.height(),
                        };
                    },
                    position: function () {
                        module.save.screenSize();
                        module.save.elementPosition();
                    },
                },

                get: {
                    pixelsPassed: function (amount) {
                        var
                            element = module.get.elementCalculations()
                        ;
                        if (amount.search('%') > -1) {
                            return element.height * (parseInt(amount, 10) / 100);
                        }

                        return parseInt(amount, 10);
                    },
                    occurred: function (callback) {
                        return module.cache.occurred !== undefined
                            ? module.cache.occurred[callback] || false
                            : false;
                    },
                    direction: function () {
                        if (module.cache.direction === undefined) {
                            module.save.direction();
                        }

                        return module.cache.direction;
                    },
                    elementPosition: function () {
                        if (module.cache.element === undefined) {
                            module.save.elementPosition();
                        }

                        return module.cache.element;
                    },
                    elementCalculations: function () {
                        if (module.cache.element === undefined) {
                            module.save.elementCalculations();
                        }

                        return module.cache.element;
                    },
                    screenCalculations: function () {
                        if (module.cache.screen === undefined) {
                            module.save.screenCalculations();
                        }

                        return module.cache.screen;
                    },
                    screenSize: function () {
                        if (module.cache.screen === undefined) {
                            module.save.screenSize();
                        }

                        return module.cache.screen;
                    },
                    scroll: function () {
                        if (module.cache.scroll === undefined) {
                            module.save.scroll();
                        }

                        return module.cache.scroll;
                    },
                    lastScroll: function () {
                        if (module.cache.screen === undefined) {
                            module.debug('First scroll event, no last scroll could be found');

                            return false;
                        }

                        return module.cache.screen.top;
                    },
                },

                setting: function (name, value) {
                    if ($.isPlainObject(name)) {
                        $.extend(true, settings, name);
                    } else if (value !== undefined) {
                        settings[name] = value;
                    } else {
                        return settings[name];
                    }
                },
                internal: function (name, value) {
                    if ($.isPlainObject(name)) {
                        $.extend(true, module, name);
                    } else if (value !== undefined) {
                        module[name] = value;
                    } else {
                        return module[name];
                    }
                },
                debug: function () {
                    if (!settings.silent && settings.debug) {
                        if (settings.performance) {
                            module.performance.log(arguments);
                        } else {
                            module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
                            module.debug.apply(console, arguments);
                        }
                    }
                },
                verbose: function () {
                    if (!settings.silent && settings.verbose && settings.debug) {
                        if (settings.performance) {
                            module.performance.log(arguments);
                        } else {
                            module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
                            module.verbose.apply(console, arguments);
                        }
                    }
                },
                error: function () {
                    if (!settings.silent) {
                        module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
                        module.error.apply(console, arguments);
                    }
                },
                performance: {
                    log: function (message) {
                        var
                            currentTime,
                            executionTime,
                            previousTime
                        ;
                        if (settings.performance) {
                            currentTime = Date.now();
                            previousTime = time || currentTime;
                            executionTime = currentTime - previousTime;
                            time = currentTime;
                            performance.push({
                                Name: message[0],
                                Arguments: [].slice.call(message, 1) || '',
                                Element: element,
                                'Execution Time': executionTime,
                            });
                        }
                        clearTimeout(module.performance.timer);
                        module.performance.timer = setTimeout(function () { module.performance.display(); }, 500);
                    },
                    display: function () {
                        var
                            title = settings.name + ':',
                            totalTime = 0
                        ;
                        time = false;
                        clearTimeout(module.performance.timer);
                        $.each(performance, function (index, data) {
                            totalTime += data['Execution Time'];
                        });
                        title += ' ' + totalTime + 'ms';
                        if (performance.length > 0) {
                            console.groupCollapsed(title);
                            if (console.table) {
                                console.table(performance);
                            } else {
                                $.each(performance, function (index, data) {
                                    console.log(data.Name + ': ' + data['Execution Time'] + 'ms');
                                });
                            }
                            console.groupEnd();
                        }
                        performance = [];
                    },
                },
                invoke: function (query, passedArguments, context) {
                    var
                        object = instance,
                        maxDepth,
                        found,
                        response
                    ;
                    passedArguments = passedArguments || queryArguments;
                    context = context || element;
                    if (typeof query === 'string' && object !== undefined) {
                        query = query.split(/[ .]/);
                        maxDepth = query.length - 1;
                        $.each(query, function (depth, value) {
                            var camelCaseValue = depth !== maxDepth
                                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
                                : query
                            ;
                            if ($.isPlainObject(object[camelCaseValue]) && (depth !== maxDepth)) {
                                object = object[camelCaseValue];
                            } else if (object[camelCaseValue] !== undefined) {
                                found = object[camelCaseValue];

                                return false;
                            } else if ($.isPlainObject(object[value]) && (depth !== maxDepth)) {
                                object = object[value];
                            } else if (object[value] !== undefined) {
                                found = object[value];

                                return false;
                            } else {
                                module.error(error.method, query);

                                return false;
                            }
                        });
                    }
                    if (isFunction(found)) {
                        response = found.apply(context, passedArguments);
                    } else if (found !== undefined) {
                        response = found;
                    }
                    if (Array.isArray(returnedValue)) {
                        returnedValue.push(response);
                    } else if (returnedValue !== undefined) {
                        returnedValue = [returnedValue, response];
                    } else if (response !== undefined) {
                        returnedValue = response;
                    }

                    return found;
                },
            };

            if (methodInvoked) {
                if (instance === undefined) {
                    module.initialize();
                }
                instance.save.scroll();
                instance.save.calculations();
                module.invoke(query);
            } else {
                if (instance !== undefined) {
                    instance.invoke('destroy');
                }
                module.initialize();
            }
        });

        return returnedValue !== undefined
            ? returnedValue
            : this;
    };

    $.fn.visibility.settings = {

        name: 'Visibility',
        namespace: 'visibility',

        debug: false,
        verbose: false,
        performance: true,

        // whether to use mutation observers to follow changes
        observeChanges: true,

        // check position immediately on init
        initialCheck: true,

        // whether to refresh calculations after all page images load
        refreshOnLoad: true,

        // whether to refresh calculations after page resize event
        refreshOnResize: true,

        // should call callbacks on refresh event (resize, etc)
        checkOnRefresh: true,

        // callback should only occur one time
        once: true,

        // callback should fire continuously when evaluates to true
        continuous: false,

        // offset to use with scroll top
        offset: 0,

        // whether to include margin in elements position
        includeMargin: false,

        // scroll context for visibility checks
        context: window,

        // visibility check delay in ms (defaults to animationFrame)
        throttle: false,

        // special visibility type (image, fixed)
        type: false,

        // z-index to use with visibility 'fixed'
        zIndex: '10',

        // image only animation settings
        transition: 'fade in',
        duration: 1000,

        // array of callbacks for percentage
        onPassed: {},

        // standard callbacks
        onOnScreen: false,
        onOffScreen: false,
        onPassing: false,
        onTopVisible: false,
        onBottomVisible: false,
        onTopPassed: false,
        onBottomPassed: false,

        // reverse callbacks
        onPassingReverse: false,
        onTopVisibleReverse: false,
        onBottomVisibleReverse: false,
        onTopPassedReverse: false,
        onBottomPassedReverse: false,

        // special callbacks for image
        onLoad: function () {},
        onAllLoaded: function () {},

        // special callbacks for fixed position
        onFixed: function () {},
        onUnfixed: function () {},

        // utility callbacks
        onUpdate: false, // disabled by default for performance
        onRefresh: function () {},

        metadata: {
            src: 'src',
        },

        className: {
            fixed: 'fixed',
            placeholder: 'constraint',
            visible: 'visible',
        },

        error: {
            method: 'The method you called is not defined.',
            visible: 'Element is hidden, you must call refresh after element becomes visible',
        },

    };
})(jQuery, window, document);
